require 'socket'
require 'iconv'
# This class will hold a Marco instance. It does the basic IRC protocol
# stuff for plug-ins. It basically works like this:
# 1. Initialize, get config options, connect to IRC server
# 2. Run commands specified in the config file, such as joining channels, identifying with services, etc.
# 3. Run the plug-in hooks on PRIVMSGs, KICKs and on every loop iteration
# 4. Repeat 3.
class IRCBot
	# This class handles plug-ins. All plug-ins are instances of this
	# class.
	# = How it works
	# Take the following sample plug-in, named "sample.rb":
	#	IRCBot::Plugin.define "sample" do
	#		help_string "I'm a sample plug-in."
	#		
	#		def command(user,target,message,hostmask)
	#			# This will run on every call to "!sample"
	#		end
	#		
	#		def privmsg_function(user,target,message,hostmask)
	#			# This will run on every message.
	#		end
	#		
	#		def loop_hook()
	#			# This will run on every iteration of the loop.
	#		end
	#	end
	# That code calls IRCBot::Plugin.define, giving the plug-in name and
	# its code as parameters. Help_string is defined via IRCBot::Plugin.def_field,
	# and is available as an instance variable.
	class Plugin
		@registered_plugins = {}
		class << self
			attr_reader :registered_plugins
			private :new
			# This will define a field for plug-in information, doing the
			# same as
			#	@var = "something"
			#	attr_getter :var
			# It's used for help information, and will be used in the future
			# to register dependencies.
			def def_field(*names)
				class_eval do
					names.each do |name|
						define_method(name) do |*args|
							case args.size
								when 0
									instance_variable_get("@#{name}")
								else
									instance_variable_set("@#{name}",*args)
							end
						end
					end
				end
			end
		end
		# This is the most important method. It creates a new plug-in and
		# registers it. It spawns a new IRCBot::Plugin class instance,
		# uses instance_eval to add the code to it, and adds the instance
		# to the <tt>registered_plugins</tt> hash.
		def self.define(name,&block)
			if !$config['global']['plugin_blacklist'].include?(name) then
				plug = new
				plug.instance_eval(&block)
				Plugin.registered_plugins[name] = plug
				puts "Registered new plugin #{name}."
			else
				puts "Ignoring blacklisted plugin #{name}."
			end
		end
		# A wrapper for IRCBot.privmsg using the current instance.
		def privmsg(target,message,prior=1)
			$ircbot.privmsg(target,message,prior)
		end
		# A wrapper for IRCBot.send using the current instance.
		def send(message)
			$ircbot.send(message)
		end
		# Returns the plug-in with that name or nil if it doesn't exist.
		def get_plugin(name)
			Plugin.registered_plugins[name]
		end
		# Registers the :help_string field.
		def_field(:help_string,:depends)
		@depends = []
	end
	
	# Starts up the bot. Loads plug-ins and sets some instance variables.
	def initialize(config,network)
		if config.class != Hash then
			puts "Not a valid config file, exiting; is #{config.class}"
#			exit(1)
		elsif config[network].class != Hash then
			puts "Not a valid network, exiting."
			exit(1)
		else
			@config = config
			@network = network
			puts "Loading plugins..."
			Dir["#{@config['global']['plugindir']}/*.rb"].each do |file|
				puts "Loading plugin #{file}..."
				load file
				puts "Plugin #{file} loaded."
			end
			puts "Checking dependencies..."
			missing_deps_plugins = {}
			for plugin in Plugin.registered_plugins
				missing_deps = []
				if plugin[1].depends
					for dependency in plugin[1].depends
						if !Plugin.registered_plugins.include?(dependency) then
							missing_deps << dependency
						end
					end
					if !missing_deps.empty? then
						missing_deps_plugins[plugin[0]] = missing_deps
					end
				end
			end
			if !missing_deps_plugins.empty? then
				puts "Dependency errors! The following plug-ins have unmet dependencies:"
				for plugin in missing_deps_plugins
					puts "#{plugin[0]}:"
					puts plugin[1].join(', ')
				end
				puts "Take note. Making a pause."
				sleep(10)
				puts "I'm assuming that you copy-pasted that. Unloading said plug-ins."
				for plugin in missing_deps_plugins
					Plugin.registered_plugins.delete plugin[0]
				end
				puts "Resuming execution."
			end
		end
		# Globals
		@users = {}
		@rawusers = {}
		@nick = $config[$network]['nickname']
		@channels = []
		@message_queue = []
		if File.exists?('version_number') then
			@version = File.read('version_number').strip
		else
			@version = 'Trunk'
		end
	end
	
	# Sends a message to the IRC server.
	def send(message)
		@socket.send("#{message}\n",0)
	end
	
	# Connects to IRC. Opens the socket, and sends USER and NICK.
	def connect()
		@socket = TCPSocket.open(@config[@network]['server'],@config[@network]['port'])
		send("USER #{@config[@network]['username']} foo baz :#{@config[@network]['realname']}, version #{if File.exists?('version_number') then File.read('version_number'); else '3'; end}")
		send("NICK #{@config[@network]['nickname']}")
	end
	
	# Handles server input. Checks its contents and treats them.
	def handle_server_input(s)
		s = s.strip.chomp
		s_spaces = s.sub(":","").split
		s_colons = s.split(":")
		message = s_colons.join(":").sub(s_colons[1],"").sub("::","")
		if s_spaces[0] == "PING" then
			send "PONG "+s_spaces[1].sub(":","")
		end
		case s_spaces[1]
			when "PRIVMSG"
				user = s_spaces[0].sub(":","").split("!")[0]
				target = s_spaces[2]
				message = s_colons.join(":").sub(s_colons[1],"").sub("::","")
				handle_message(user,target,message,s_spaces[0].sub(":",""))
			when "INVITE"
				send("JOIN #{s_spaces[3]}")
			when 'TOPIC' #TODO: handle this
			
			when 'KICK'
				if s_spaces[3] == @config[@network]['nickname'] then
					send("JOIN #{s_spaces[2]}")
				else	
					send("NAMES #{s_spaces[2].sub(":","")}")
					kicker	=	s_spaces[0].sub(':','').split('!')[0].downcase.strip
					puts "kicker: '#{kicker}'"
					channel	=	s_spaces[2].downcase.strip
					puts "channel: '#{channel}'"
					kicked	=	s_spaces[3].downcase.strip
					puts "kicked: '#{kicked}'"
					message	=	s.sub("#{s_spaces[0]} #{s_spaces[1]} #{s_spaces[2]} #{s_spaces[3]} :",'').strip.downcase.sub(':','')
					puts "message: '#{message}'"
					for plugin in Plugin.registered_plugins
						if plugin[1].methods.include?('kick_function') then
							plugin[1].kick_function(kicker,channel,kicked,message)
						end
					end
				end
			when 'MODE' #TODO: handle this?
				if s_spaces[4] != nil then
					if s_spaces[4].strip == @nick then
						
					end
				end
				for channel in @channels
					send("NAMES #{channel}")
				end
			when 'PART'
				if s_spaces[0].split("!")[0] == @nick then
					@channels.delete(s_spaces[2].downcase)
				end
				send("NAMES #{s_spaces[2].sub(":","")}")
			when 'JOIN'
				if s_spaces[0].split("!")[0] == @nick then
					@channels.push(s_spaces[2].sub(":","").downcase)
				end
			when 'QUIT'
				for channel in @channels
					send("NAMES #{channel}")
				end
			when 'NICK'
				newnick = s_spaces[2].sub(":","").strip
				if s_spaces[0].sub(':','').split("!")[0].downcase == @nick.downcase then
					@nick = newnick
					$config[$network]['nickname'] = newnick
				end
				for channel in @channels
					send("NAMES #{channel}")
				end
			
			# Server messages go after this point
			
			when '375'
				startup_routine()
			when '353'
				puts "recieving NAMES list"
				userlist = s_spaces
				userlist = userlist.join(" ")
				userlist = userlist.sub("#{s_spaces[0]} #{s_spaces[1]} #{s_spaces[2]} #{s_spaces[3]} #{s_spaces[4]} ","")
				rawuserlist = userlist.downcase.sub(":","").split
				userlist.gsub!(/(\+|%|@|&|:)/,"")
				userlist = userlist.downcase.split
				if @users[s_spaces[4].downcase].frozen? or @rawusers[s_spaces[4].downcase] == nil then
					puts "Clearing list"
					@users[s_spaces[4].downcase] = []
					@rawusers[s_spaces[4].downcase] = []
				end
				puts "Appending list"
				@users[s_spaces[4].downcase] = @users[s_spaces[4].downcase] + userlist
				@rawusers[s_spaces[4].downcase] = @rawusers[s_spaces[4].downcase] + rawuserlist	
				puts userlist.join(", ")
			when '366'
				puts "Locking #{s_spaces[3].downcase}'s userlist"
				@users[s_spaces[3].downcase].freeze
			when '433'
				send("NICK #{@nick}_")
				@nick = $config[$network]['nickname'] = "#{@nick}_"
		end
	end
	
	# Sends the startup commands as supplied in the config.
	def startup_routine()
		for command in @config[@network]['commands']
			send command.gsub("$nick",@nick)
		end
	end
	
	# Treats PRIVMSGs. Checks if they start with the command identifier,
	# checking plug-ins for handlers and calling them, and calls the plug-ins'
	# on_privmsg handlers.
	def handle_message(user,target,message,usermask)
		if !target.start_with?("#") then
			target = user
		end
		for plugin in Plugin.registered_plugins
			if plugin[1].methods.include?('privmsg_function') then
				plugin[1].privmsg_function(user,target,message,usermask)
			end
		end
		if message.start_with?(@config['global']['command_identifier']) then
			message = message.sub(@config['global']['command_identifier'],"").strip
			command = message.split[0]
			message = message.sub(command,"").strip
			if Plugin.registered_plugins[command] != nil and Plugin.registered_plugins[command].methods.include?('command') then
				puts "Running plugin command"
				Plugin.registered_plugins[command].command(user,target,message,usermask)
			end
		end
	end
	# Dummy function, deprecated.
	def log(*args) # Dummy
		
	end
	# Keeps the bot alive. Checks for socket activity, and passes the input
	# to IRCBot.handle_server_input if so, manages the PRIVMSG queue, and
	# calls plug-ins' main_loop hooks.
	def main_loop() #FIXME: high CPU usage?
			while true
				trap('INT'){send("QUIT")}
				trap('TERM'){send("QUIT")}
				trap('KILL'){send("QUIT")}
				ready = select([@socket],nil,nil,0.1)
				if ready then
					return if @socket.eof
					s = @socket.gets
					puts s
					handle_server_input(s)
				end
				if !@message_queue.empty? then
					message = @message_queue[0]
					@message_queue.delete message
					case message[2]
						when 2
							sleep 1.5
						when 1
							sleep 0.5
					end
					send_message(message[0],message[1])
				end
				for plugin in Plugin.registered_plugins
					if plugin[1].methods.include?('loop_hook') then
						plugin[1].loop_hook
					end
				end
			end
	end
	# Adds a message to the outgoing queue, splitting every 380 chars
	# respecting word limits.
	def privmsg(target,message,priority=1)
		if message.class == String then
			if message.length >= 380 then
				output_array = []
				while !message.empty?
					output = ''
					for word in message.strip.split
						break if output.length >= 380
						output << word+' '
					end
					message = message.sub(output.strip,'').strip
					output_array << output.strip
				end
				for item in output_array
					@message_queue.push [target,item,priority]
				end
			else
				@message_queue.push [target,message,priority]
			end
		end
	end
	
	# Actually sends PRIVMSGs.
	def send_message(target,message)
		send("PRIVMSG #{target} :#{message}")
	end
	private :send_message
	attr_reader :config,:users,:channels,:version
	attr_accessor :socket,:message_queue
end
